package com.study.intercept;

import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Properties;

import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.binding.MapperMethod.ParamMap;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import com.study.entity.db3.Account;
import com.study.entity.db3.BaseEntity;

/**
  * mybatis 自定义拦截器<br>
  * 三步骤：<br>
  *  1 实现 {@link Interceptor} 接口<br>
  *  2 添加拦截注解 {@link Intercepts}<br>
  *  3 配置文件中添加拦截器<br>
  *<br>
  * 1 实现 {@link Interceptor} 接口<br>
  *      具体作用可以看下面代码每个方法的注释<br>
  * 2 添加拦截注解 {@link Intercepts}<br>
  *      mybatis 拦截器默认可拦截的类型只有四种，即四种接口类型 Executor、StatementHandler、ParameterHandler 和 ResultSetHandler<br>
  *      对于我们的自定义拦截器必须使用 mybatis 提供的注解来指明我们要拦截的是四类中的哪一个类接口<br>
  *      具体规则如下：<br>
  *          a：Intercepts 拦截器: 标识我的类是一个拦截器<br>
            b：Signature 署名: 则是指明我们的拦截器需要拦截哪一个接口的哪一个方法<br>
  *              type    对应四类接口中的某一个，比如是 Executor<br>
  *              method  对应接口中的哪类方法，比如 Executor 的 update 方法<br>
  *              args    对应接口中的哪一个方法，比如 Executor 中 query 因为重载原因，方法有多个，args 就是指明参数类型，从而确定是哪一个方法<br>
  * 3 配置文件中添加拦截器<br>
  *      拦截器其实就是一个 plugin，在 mybatis 核心配置文件中我们需要配置我们的 plugin ：<br>
  *          <plugin interceptor="liu.york.mybatis.study.plugin.MyInterceptor"><br>
  *              <property name="username" value="LiuYork"/><br>
  *              <property name="password" value="123456"/><br>
  *          </plugin><br>
  *
  * 拦截器顺序<br>
  * 1 不同拦截器顺序：<br>
  *      Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler<br>
  *<br>
  * 2 对于同一个类型的拦截器的不同对象拦截顺序：<br>
  *      在 mybatis 核心配置文件根据配置的位置，拦截顺序是 从上往下<br>
  */
@Component
@Intercepts({ @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }) })
public class MybatisInterceptor implements Interceptor {

	private static final Logger LOGGER = LoggerFactory.getLogger(MybatisInterceptor.class);

	/**
	 * 这个方法很好理解<br>
	 * 作用只有一个：我们不是拦截方法吗，拦截之后我们要做什么事情呢？<br>
	 * 这个方法里面就是我们要做的事情<br>
	 * <br>
	 * 解释这个方法前，我们一定要理解方法参数 {@link Invocation} 是个什么鬼？<br>
	 * 1 我们知道，mybatis拦截器默认只能拦截四种类型 Executor、StatementHandler、ParameterHandler 和
	 * ResultSetHandler<br>
	 * 2 不管是哪种代理，代理的目标对象就是我们要拦截对象，举例说明：<br>
	 * 比如我们要拦截 {@link Executor#update(MappedStatement ms, Object parameter)} 方法，<br>
	 * 那么 Invocation 就是这个对象，Invocation 里面有三个参数 target method args<br>
	 * target 就是 Executor<br>
	 * method 就是 update<br>
	 * args 就是 MappedStatement ms, Object parameter<br>
	 * <br>
	 * 如果还是不能理解，我再举一个需求案例：看下面方法代码里面的需求<br>
	 * <br>
	 * 该方法在运行时调用<br>
	 */
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
		LOGGER.info("----sqlId----" + mappedStatement.getId());

		// sql类型：insert update select delete
		SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
		// 获取参数
		Object parameter = invocation.getArgs()[1];
		LOGGER.info("----sqlCommandType----" + sqlCommandType);
		if (parameter == null) {
			return invocation.proceed();
		}

		// 当SQL为新增或者更新类型时，自动填充操作人、时间等相关信息
		if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
			if (parameter instanceof MapperMethod.ParamMap) {
				MapperMethod.ParamMap<?> map = (ParamMap<?>) parameter;
				List<?> list = (List<?>) map.get("list");
				if (list != null) {
					for (Object o : list) {
						setParameter(o, sqlCommandType);
					}
				}
			} else {
				setParameter(parameter, sqlCommandType);
			}
		}
		return invocation.proceed();
	}

	/**
	 * 这个方法也很好理解<br>
	 * 作用就只有一个：那就是Mybatis在创建拦截器代理时候会判断一次，当前这个类 MyInterceptor 到底需不需要生成一个代理进行拦截，<br>
	 * 如果需要拦截，就生成一个代理对象，这个代理就是一个 {@link Plugin}，它实现了jdk的动态代理接口
	 * {@link InvocationHandler}，<br>
	 * 如果不需要代理，则直接返回目标对象本身<br>
	 * <br>
	 * Mybatis为什么会判断一次是否需要代理呢？<br>
	 * 默认情况下，Mybatis只能拦截四种类型的接口：Executor、StatementHandler、ParameterHandler 和
	 * ResultSetHandler<br>
	 * 通过 {@link Intercepts} 和 {@link Signature} 两个注解共同完成<br>
	 * 试想一下，如果我们开发人员在自定义拦截器上没有指明类型，或者随便写一个拦截点，比如Object，那Mybatis疯了，难道所有对象都去拦截<br>
	 * 所以Mybatis会做一次判断，拦截点看看是不是这四个接口里面的方法，不是则不拦截，直接返回目标对象，如果是则需要生成一个代理<br>
	 * <br>
	 * 该方法在 mybatis 加载核心配置文件时被调用<br>
	 */
	@Override
	public Object plugin(Object target) {
		/**
		 * 看了这个方法注释，就应该理解，这里的逻辑只有一个，就是让mybatis判断，要不要进行拦截，然后做出决定是否生成一个代理<br>
		 * <br>
		 * 下面代码什么鬼，就这一句就搞定了？<br>
		 * Mybatis判断依据是利用反射，获取这个拦截器 MyInterceptor 的注解 Intercepts和Signature，然后解析里面的值，<br>
		 * 1 先是判断要拦截的对象是四个类型中 Executor、StatementHandler、ParameterHandler、
		 * ResultSetHandler 的哪一个<br>
		 * 2 然后根据方法名称和参数(因为有重载)判断对哪一个方法进行拦截 Note：mybatis可以拦截这四个接口里面的任一一个方法<br>
		 * 3 做出决定，是返回一个对象呢还是返回目标对象本身(目标对象本身就是四个接口的实现类，我们拦截的就是这四个类型)<br>
		 * <br>
		 * 好了，理解逻辑我们写代码吧~~~ What !!! 要使用反射，然后解析注解，然后根据参数类型，最后还要生成一个代理对象<br>
		 * 我一个小白我怎么会这么高大上的代码嘛，怎么办？<br>
		 * <br>
		 * 那就是使用下面这句代码吧 哈哈<br>
		 * mybatis 早就考虑了这里的复杂度，所以提供这个静态方法来实现上面的逻辑<br>
		 */
		return Plugin.wrap(target, this);
	}

	/**
	 * 这个方法最好理解，如果我们拦截器需要用到一些变量参数，而且这个参数是支持可配置的，<br>
	 * 类似Spring中的@Value("${}")从application.properties文件获取<br>
	 * 这个时候我们就可以使用这个方法<br>
	 * <br>
	 * 如何使用？<br>
	 * 只需要在 mybatis 配置文件中加入类似如下配置，然后 {@link Interceptor#setProperties(Properties)}
	 * 就可以获取参数<br>
	 * <plugin interceptor="liu.york.mybatis.study.plugin.MyInterceptor"><br>
	 * <property name="username" value="LiuYork"/><br>
	 * <property name="password" value="123456"/><br>
	 * </plugin><br>
	 * 方法中获取参数：properties.getProperty("username");<br>
	 * <br>
	 * 问题：为什么要存在这个方法呢，比如直接使用 @Value("${}") 获取不就得了？<br>
	 * 原因是 mybatis 框架本身就是一个可以独立使用的框架，没有像 Spring 这种做了很多依赖注入的功能<br>
	 * <br>
	 * 该方法在 mybatis 加载核心配置文件时被调用 <br>
	 */
	@Override
	public void setProperties(Properties properties) {
		Interceptor.super.setProperties(properties);
		// 业务处理逻辑
	}

	private void setParameter(Object parameter, SqlCommandType sqlCommandType) {
		Class<? extends Object> clazz = parameter.getClass();
		Field[] declaredFields;
		// 如果常用字段提取了公共类BaseEntity
		// 判断BaseEntity 是否是父类
		if (BaseEntity.class.isAssignableFrom(clazz)) {
			// 从父类中获取私有成员变量
			declaredFields = clazz.getSuperclass().getDeclaredFields();
		} else {
			// 获取私有成员变量
			declaredFields = clazz.getDeclaredFields();
		}
		if (SqlCommandType.INSERT.equals(sqlCommandType)) {
			for (Field field : declaredFields) {
				try {
					if ("createTime".equals(field.getName())) {// 可以采用注解的形式
						field.setAccessible(true);
						field.set(parameter, LocalDateTime.now());
						field.setAccessible(false);
					}

					if ("createAccountId".equals(field.getName())) {
						Object account = RequestContextHolder.getRequestAttributes().getAttribute("account",
								RequestAttributes.SCOPE_SESSION);
						LOGGER.info("account=" + account);
						if (account != null) {
							Long accountId = ((Account) account).getAccountId();
							field.setAccessible(true);
							field.set(parameter, accountId);
							field.setAccessible(false);
						}
					}
				} catch (Exception e) {
					LOGGER.error("failed to insert data, exception = " + e.getMessage());
				}
			}
		}

		if (SqlCommandType.UPDATE.equals(sqlCommandType)) {
			for (Field field : declaredFields) {
				try {
					if ("modifiedTime".equals(field.getName())) {
						field.setAccessible(true);
						field.set(parameter, LocalDateTime.now());
						field.setAccessible(false);
					}

					if ("modifiedAccountId".equals(field.getName())) {
						Object account = RequestContextHolder.getRequestAttributes().getAttribute("account",
								RequestAttributes.SCOPE_SESSION);
						LOGGER.info("account=" + account);
						if (account != null) {
							Long accountId = ((Account) account).getAccountId();
							field.setAccessible(true);
							field.set(parameter, accountId);
							field.setAccessible(false);
						}
					}
				} catch (Exception e) {
					LOGGER.error("failed to update data, exception = " + e.getMessage());
				}
			}
		}
	}

}
